package com.ofwiki.demo.scheduler;

import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.ofwiki.demo.model.JobLogs;
import com.ofwiki.demo.model.Jobs;
import com.ofwiki.demo.scheduler.common.*;
import com.ofwiki.demo.service.JobLogsService;
import com.ofwiki.demo.service.JobsService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.scheduling.config.TriggerTask;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

/**
 * @author HuangJS
 * @date 2021-12-21 10:47 上午
 */
@Component
public class JobRegisterHandler extends AbstractDynamicCronHandler implements SmartInitializingSingleton, ApplicationContextAware {
    private final static Logger logger = LoggerFactory.getLogger(JobRegisterHandler.class);
    @Value("${spring.application.name}")
    private String serviceName;
    @Value("${server.address:}")
    private String ip;

    private ApplicationContext applicationContext;
    private static ConcurrentMap<String, JobMethodRunnable> JOB_RUNNABLE_REPOSITORY = new ConcurrentHashMap<>();
    
    @Autowired
    protected DynamicCronScheduleTaskManager dynamicCronScheduleTaskManager;
    @Autowired
    private JobsService jobsService;
    @Autowired
    private JobLogsService jobLogsService;
    @Autowired
    private RedissonClient redissonClient;
    private Map<Long, Jobs> jobsMap = new ConcurrentHashMap<>();
    
    private List<JobHandler> jobHandlers = new ArrayList<>();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    
    @Override
    public void afterSingletonsInstantiated()  {
        initRepository();
        
        loadJobHandler();

        saveJobHandlerMethodRepository();
    }
    
    private void saveJobHandlerMethodRepository() {
        Map<String, Jobs> map = this.jobsMap.values().stream().collect(Collectors.toMap(x -> x.getName(), x -> x, (o, o2) -> o));
        List<Jobs> list = this.jobHandlers.stream().filter(x -> !map.containsKey(x.value())).map(x -> {
            Jobs jobs = new Jobs();
            jobs.setServiceName(this.serviceName);
            jobs.setName(x.value());
            jobs.setDescription(x.desc());
            return jobs;
        }).collect(Collectors.toList());
        
        try {
            if (!list.isEmpty()) {
                jobsService.saveBatch(list);
            }

            Set<String> names = this.jobHandlers.stream().map(x -> x.value()).collect(Collectors.toSet());
            List<Jobs> unmounts = this.jobsMap.values().stream().filter(x -> !names.contains(x.getName())).map(x -> {
                x.setMount(false);
                return x;
            }).collect(Collectors.toList());
            if (!unmounts.isEmpty()) {
                jobsService.updateBatchById(unmounts);
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        jobHandlers.clear();
        jobHandlers = null;
    }
    
    public void loadJobHandler() {
        List<Jobs> jobs = jobsService.listByServiceName(serviceName);
        cancelOldJobHandler(jobs);
        
        jobs.forEach(item -> {
            Jobs oldJob = jobsMap.put(item.getId(), item);
            boolean scheduleCronNotChanged = oldJob == null || oldJob.getCronExpression().equals(item.getCronExpression());
            registerJob(item, scheduleCronNotChanged);
        });
    }
    
    private void cancelOldJobHandler(List<Jobs> schedules) {
        Set<Long> oldIds = jobsMap.keySet();
        if (!oldIds.isEmpty()) {
            Set<Long> newScheduleIds = schedules.stream().map(Jobs::getId).collect(Collectors.toSet());
            for (Long id : oldIds) {
                if (!newScheduleIds.contains(id)) {
                    jobsMap.remove(id);
                    cancelJobHandler(id);
                }
            }
        }
    }
    
    private void initRepository() {
        String[] beanDefinitionNames = this.applicationContext.getBeanNamesForType(Object.class, false, true);
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = this.applicationContext.getBean(beanDefinitionName);
            Map<Method, JobHandler> annotatedMethods = null;
            try {
                annotatedMethods = MethodIntrospector.selectMethods(
                        bean.getClass(),
                        (MethodIntrospector.MetadataLookup<JobHandler>) method -> AnnotatedElementUtils.findMergedAnnotation(method, JobHandler.class)
                );
            } catch (Throwable ex) {
                logger.error("动态任务注册 method-jobHandler resolve error for bean[" + beanDefinitionName + "].", ex);
            }
            if (annotatedMethods==null || annotatedMethods.isEmpty()) {
                continue;
            }
            
            for (Map.Entry<Method, JobHandler> methodJobEntry : annotatedMethods.entrySet()) {
                Method executeMethod = methodJobEntry.getKey();
                JobHandler jobHandler = methodJobEntry.getValue();
                registerJobHandler(jobHandler, bean, executeMethod);
            }
        }
        
    }
    public static JobMethodRunnable loadJobHandler(String name){
        return JOB_RUNNABLE_REPOSITORY.get(name);
    }
    public static JobMethodRunnable registerJobHandler(String name, JobMethodRunnable jobHandler){
        logger.info("动态任务发现 jobHandler name:{}", name);
        return JOB_RUNNABLE_REPOSITORY.put(name, jobHandler);
    }
    
    private void registerJobHandler(JobHandler jobHandler, Object bean, Method executeMethod) {
        if (jobHandler == null) {
            return;
        }
        
        String name = jobHandler.value();
        Class<?> clazz = bean.getClass();
        String methodName = executeMethod.getName();
        if (name.trim().length() == 0) {
            throw new RuntimeException("动态任务注册 method-jobHandler name invalid, for[" + clazz + "#" + methodName + "] .");
        }
        if (loadJobHandler(name) != null) {
            throw new RuntimeException("动态任务注册 jobHandler[" + name + "] 命名冲突.");
        }
        
        executeMethod.setAccessible(true);
        jobHandlers.add(jobHandler);
        registerJobHandler(name, new JobMethodRunnable(bean, executeMethod));
    }
    
    
    private void registerJob(Jobs job, boolean scheduleCronNotChanged) {
        Jobs oldJob = jobsMap.put(job.getId(), job);
        if (!job.getStatus() || !job.getMount()) {
            cancelJobHandler(job.getId());
            return;
        }

        // 任务存在 且触发时机没变，则不需要重新生成任务
        if (oldJob != null && scheduleCronNotChanged && dynamicCronScheduleTaskManager.contains(job.getId())) {
            return;
        }
        
        Runnable runnable = generateBeanRunnable(job.getId(), job.getName());
        if (runnable == null) {
            return;
        }
        
        TimeZone timeZone = job.getTimeZone()==null || "".equals(job.getTimeZone()) ?
                TimeZone.getDefault()
                : TimeZone.getTimeZone(job.getTimeZone());
        
        DynamicCronTrigger trigger = new DynamicCronTrigger(job.getId(), timeZone, this);
        dynamicCronScheduleTaskManager.addTriggerTask(job.getId(), new TriggerTask(runnable,trigger));
        logger.info("动态任务注册 register success,jobId:{},jobName:{}", job.getName());
    }
    
    public void triggerJobHandler(Long id, String jobData) {
        Jobs job = jobsMap.get(id);
        if (job != null) {
            Runnable runnable = generateBeanRunnable(job.getId(), job.getName(), jobData);
            if (runnable != null) {
                runnable.run();
            }
        }
    }
    
    private void cancelJobHandler(Long id) {
        if (dynamicCronScheduleTaskManager.contains(id)) {
            dynamicCronScheduleTaskManager.removeTriggerTask(id);
        }
    }
    
    private Runnable generateBeanRunnable(Long jobId,String jobHandler)  {
        return generateBeanRunnable(jobId,jobHandler,null);
    }
    private Runnable generateBeanRunnable(Long jobId,String jobHandler,String jobData)  {
        try {
            JobMethodRunnable iJobHandler = loadJobHandler(jobHandler);
            if (iJobHandler == null) {
                logger.warn("动态任务注册 jobId:{},not fund method-jobHandler:{}", jobId, jobHandler);
                cancelJobHandler(jobId);
                return null;
            }
            
            return () -> {
                JobContext.setJobContext(null);
                
                Jobs job = jobsMap.get(jobId);
                if (job == null) {
                    logger.info("job不存在,jobId:{}", job.getId());
                    return;
                }
                
                RLock lock = null;
                try {
                    if (!job.getConcurrent()) {
                        lock = redissonClient.getLock("schedule_job:lock:" + job.getId());
                        if (!lock.tryLock()) {
                            logger.info("jobHandler 获取不到锁,jobId:{},jobHandler:{}", job.getId(), job.getName());
                            lock = null;
                            return;
                        }
                    }

                    String jobParam = jobData == null || "".equals(jobData) ? job.getParam() : jobData;
                    
                    JobContext.setJobContext(new JobContext(job.getId(), jobParam));
                    saveProcess(job, jobParam);

                    iJobHandler.execute();
                } catch (Exception e) {
                    JobContext jobContext = JobContext.getJobContext();
                    if (jobContext != null) {
                        jobContext.setTriggerCode(JobContext.FAIL);
                    }
                    logger.error(e.getMessage(), e);
                } finally {
                    updateProcess();
                    if (lock != null && lock.isLocked()) {
                        lock.unlock();
                    }
                }
            };
        } catch (Exception e) {
            logger.warn("动态任务注册失败 jobId:{},jobHandler:{}", jobId, jobHandler);
        }
        return null;
    }
    
    private void saveProcess(Jobs job, String jobParam) {
        if (!job.getLogDurable()) {
            return;
        }
        JobLogs log = new JobLogs();
        log.setId(IdWorker.getId());
        log.setJobId(job.getId());
        log.setJobName(job.getName());
        log.setStartTime(LocalDateTime.now());
        log.setServiceName(this.serviceName);
        log.setExecHost(getIpAddress());
        log.setExecStatus(0);
        log.setResponseCode(0);
        log.setParam(jobParam);
        jobLogsService.save(log);
        JobContext.getJobContext().setProcessId(log.getId());
    }
    
    private void updateProcess() {
        JobContext jobContext = JobContext.getJobContext();
        if (jobContext == null || jobContext.getProcessId() == null) {
            return;
        }
        
        try {
            JobLogs process = new JobLogs();
            process.setId(jobContext.getProcessId());
            process.setEndTime(LocalDateTime.now());
            process.setExecStatus(jobContext.getTriggerCode());
            process.setResponseCode(jobContext.getHandleCode());
            process.setResponseMsg(jobContext.getHandleMsg());
            jobLogsService.updateById(process);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }
    
    @Override
    public String getCronExpression(Object taskId) {
        return Optional.ofNullable(jobsMap.get(taskId)).map(x->x.getCronExpression()).orElse(null);
    }
    
    public String getIpAddress() {
        try {
            if (ip == null || "".equals(ip)) {
                ip = InetAddress.getLocalHost().getHostAddress();
            }
        } catch (UnknownHostException e) {
            logger.error(e.getMessage(), e);
        }
        return ip;
    }
}
